<template>
  <component :is="asyncComp"/>
  <AsyncCompA />
</template>
<script>
import { shallowRef } from 'vue'

  // 重试机制的实现
  function fetch(){
    return new Promise((res,rej)=>{
      // 请求会在 1s 以后 失败
      setTimeout(()=>{
        rej('err')
      },1000)
    })
  }

  // load 函数接收一个 onError 回调函数
  function load(onError){
    // 请求接口，得到 Promise 实例
    const p = fetch()
    // 捕获错误
    return p.catch((err)=>{
      // 当错误发生时，返回一个新的 Promise 实例， 并调用 onError 回调， 同时将 retry 函数作为 onError 回调的参数
      return new Promise((res,rej)=>{
        // retry 函数， 用来执行重试的函数，执行该函数会重新调用 load 函数 并发起请求
        const retry = () => res(load(onError))
        const fail = () => rej(err)
        onError(retry,fail)
      })
    })
  }

// defineAsyncComponet 函数用于定义一个 异步组件， 接收一个异步组件加载器作为参数
function defineAsyncComponet(options){
  // options 可以是配置项(对象)，也可以是加载器 ()=>import('a') 函数
  if(typeof options === 'function'){
    // 如果 options 是加载器， 则将其格式化为配置项
    options ={
      loader:options,
    }
  }

  const { loader } = options 

  // 一个变量， 用来存储异步加载的组件
  let InnerComp = null

  // 记录重试次数
  let retries = 0
  // 封装 load 函数用来加载异步组件
  function load(){
    return loader()
    .catch((err)=>{
      // 如果用户指定了 onError 回调， 则将该控制权交给用户
      if(options.onError){
        // 返回一个新的 Promise
        return new Promise((res,rej)=>{
          // 重试
          const retry = () => {
            res(load())
            retries++
          }
          // 失败
          const fail = () => rej(err)
          // 作为 onError 回调函数的参数， 让用户来决定下一步怎么做
          options.onError(retry,fail,retries)
        })
      }else{throw error}
    })
  }
  // 返回一个包装组件
  return{
    name:'AsnycComponentWrapper',
    setup(){
      // 异步组件是否加载成功
      const loaded = ref(false)
      // 定义 error ， 当错误发生时，用来存储错误对象
      const error = shallowRef(null)
      // 一个标志，代表是否正在加载，默认为 false
      const loading = ref(false)
      let loadingTimer = null
      //如果配置中 存在 delay， 则开启一个定时器计时，当延迟到时后，将 loading.value 设置为 true
      if(options.delay){
        loadingTimer = setTimeout(()=>{
          loading.value = true
        },options.delay)
      }else{
        // 如果配置项中没有 delay， 则直接标记为 加载中
        loading.value = true
      }
      // 调用 load 加载组件
      // 加载成功以后，将加载成功的组件赋值给 InnerComp， 并将 loaded 标记为 true， 代表加载成功
      load().then(c=>{
        InnerComp = c
        loaded.value = true
      }).catch((err)=>error.value = err)
      .finally(()=>{
        loading.value = false,
        // 加载完毕，无论成功与否， 都要清除延时定时器
        clearTimeout(loadingTimer)
      })
      let timer = null
      if(options.timeout){
        // 如果指定了超时时长， 则开启一个定时器计时
        timer = setTimeout(()=>{
          // 超时后 创建一个错误对象，并复制给 error.value
          const err = new Error(`Async Component timed out after ${options.timeout}ms`)
          error.value = err
        },options.timeout)
      }
      // 包装组件被卸载时， 清楚定时器
      onUnmounted(()=>clearTimeout(timer))
      // 占位内容
      const placeholder = { type:Text, children:''}
      return () => {
        if(loaded.value){
          // 如果组件异步加载成功，则渲染被加载的组件
          return { type:InnerComp } 
        } else if(error.value && options.errorComponent){
          // 只有当错误存在，并且用户指定了 Error 组件， 则渲染该组件， 同时将 errpr 作为 props 传递
          return { type: options.errorComponent, props:{ error:error.value} }
        }else if(loading.value && options.loadingComponent){
          // 如果异步加载组件正在加载，并且用户指定了 loading 组件， 则渲染 loading 组件
          return { type:options.loadingComponent}
        }else{
          // 否则渲染一个占位内容
        return placeholder
        }
      }
    }
  }
}
export default defineComponent({
  components:{
    // 使用 defineAsyncComponet 定义一个异步组件， 它接受一个加载器作为参数
    AsyncCompA:defineAsyncComponet({
      loader:()=>import('a'), // 动态导入异步组件  import 语法 返回一个 promise
      timeout:2000, // 超时时长 单位 ms
      errorComponent: MyErrorComp ,//指定出错时要渲染的组件
      delay: 200, // 延迟200ms 展示 loading 组件
      loadingComponent:{
        setup(){
          return () => {
            return {type:'h2',children:'Loading...'}
          }
        }
      }
    })
  },
  setup(){
    const asyncComp = shallowRef(null)
    // 异步加载组件
    import('a.vue').then(a=>asyncComp.value = a)
    return{
      asyncComp,
      AsyncCompA
    }
  }
})
</script>